package toolib.geom;

/**
 * Circle by midpoint and a radius.
 * 
 * @author Steffen Fiedler
 */
public class Circle {
	
	/**
	 * Radius of the circle. Means distance from the 
	 * midpoint (<code>position</code>) to the outline.
	 */
	public float radius;
	
	/**
	 * Midpoint of the circle.
	 */
	public V2f position;
	
	/**
	 * Creates a new circle positioned at (0,0) with 
	 * a magnitude of zero.
	 */
	public Circle () {
		position	= new V2f ();
		radius		= 0;
	}
	
	/**
	 * Creates a new circle positioned at the coordinates 
	 * (x,y) with a magnitude of zero.
	 * 
	 * @param theX Point x-coordinate.
	 * @param theY Point y-coordinate.
	 */
	public Circle (float theX, float theY) {
		position	= new V2f (theX, theY);
		radius		= 0;
	}
	
	/**
	 * Creates a new circle positioned at the coordinates 
	 * (x,y) with a specific magnitude - defined by the radius.
	 * 
	 * @param theX Point x-coordinate.
	 * @param theY Point y-coordinate.
	 * @param theRadius Radius of the circle.
	 */
	public Circle (float theX, float theY, float theRadius) {
		position	= new V2f (theX, theY);
		radius		= theRadius;
	}
	
	/**
	 * Creates a new circle positioned by the given vector 
	 * with a magnitude of zero.
	 * 
	 * @param thePosition Position vector.
	 */
	public Circle (V2f thePosition) {
		position	= thePosition;
		radius		= 0;
	}
	
	/**
	 * Creates a new circle positioned by the given vector 
	 * with a specific magnitude - defined by the radius.
	 * @param thePosition
	 * @param theRadius
	 */
	public Circle (V2f thePosition, float theRadius) {
		position	= thePosition;
		radius		= theRadius;
	}
	
	/**
	 * 
	 * @param theCircle
	 * @return
	 */
	public final boolean contains (Circle theCircle) {
		if (theCircle != null) {
			return (position.distance (theCircle.position) <= radius - theCircle.radius) ? true : false;
		}
		return false;
	}
	
	/**
	 * Diameter or magnitude of the circle.
	 * @return Diameter of the circle.
	 */
	public final float diameter () {
		return radius * 2;
	}
	
	/**
	 * Equipollency of the circle to another 
	 * circle instance.
	 * 
	 * @param theCircle Circle to compare with.
	 * 
	 * @return Returns <code>true</code> if the 
	 * position and the radius are identical; 
	 * otherwise <code>false</code>.
	 */
	public final boolean equals (Circle theCircle) {
		return (position.equals (theCircle.position) && radius == theCircle.radius) ? true : false;
	}
	
	/**
	 * Equipollency of the circle to another 
	 * circle defined by a position vector and 
	 * a numerical value for the radius.
	 * 
	 * @param thePosition Position vector of the 
	 * circle to compare with.
	 * @param theRadius Radius of the circle to 
	 * compare with.
	 * 
	 * @return Returns <code>true</code> if the 
	 * position and the radius are identical; 
	 * otherwise <code>false</code>.
	 */
	public final boolean equals (V2f thePosition, float theRadius) {
		return (position.equals (thePosition) && radius == theRadius) ? true : false;
	}
	
	/**
	 * Equipollency of the circle to another 
	 * circle defined by x and y position 
	 * coordinates and a numerical value for 
	 * the radius.
	 * 
	 * @param theX Point x-coordinate.
	 * @param theY Point y-coordinate.
	 * @param theRadius Radius.
	 * 
	 * @return Returns <code>true</code> if the 
	 * position and the radius are identical; 
	 * otherwise <code>false</code>.
	 */
	public final boolean equals (float theX, float theY, float theRadius) {
		return (theX == position.x && theY == position.y && radius == theRadius) ? true : false;
	}
	
	/**
	 * Checks whether a given point is located on the 
	 * shape of the circle or not. The distance to the 
	 * midpoint of the circle must be lesser than the 
	 * radius of the circle.
	 * 
	 * @param theX Point x-coordinate.
	 * @param theY Point y-coordinate.
	 * 
	 * @return Returns <code>true</code> if the given 
	 * coordinates define a point on the shape of the 
	 * circle; otherwise <code>false</code>.
	 */
	public final boolean isPointOnShape (float theX, float theY) {
		return (position.distance (theX, theY) <= radius) ? true : false;
	}
	/**
	 * Checks whether a given vector is located on the 
	 * shape of the circle or not. The distance to the 
	 * midpoint of the circle must be lesser than the 
	 * radius of the circle.
	 * 
	 * @param theVector Vector describing the point.
	 * 
	 * @return Returns <code>true</code> if a given vector 
	 * defines a point on the shape of the circle; otherwise 
	 * <code>false</code> will be returned.
	 */
	public final boolean isPointOnShape (V2f theVector) {
		return isPointOnShape (theVector.x, theVector.y);
	}
	
	/**
	 * Checks whether a given point is on the outline of 
	 * the circle instance. The distance to the midpoint 
	 * of the circle has to be the radius.
	 * 
	 * @param theX Point x-coordinate.
	 * @param theY Point y-coordinate.
	 * 
	 * @return Returns <code>true</code> if the given 
	 * coordinates define a point that is on the outline 
	 * of the circle; otherwise <code>false</code>.
	 */
	public final boolean isPointOnOutline (float theX, float theY) {
		return (position.distance (theX, theY) == radius) ? true : false;
	}
	
	/**
	 * Checks whether a given vector is on the outline of 
	 * the circle instance. The distance to the midpoint 
	 * of the circle has to be the radius.
	 * 
	 * @param theVector Vector describing the point.
	 * 
	 * @return Returns <code>true</code> if the given 
	 * vector defines a point that is on the outline 
	 * of the circle; otherwise <code>false</code>.
	 */
	public final boolean isPointOnOutline (V2f theVector) {
		return isPointOnOutline (theVector.x, theVector.y);
	}
	
	/**
	 * Returns a full copy of the circle instance.
	 * @return Clone of the circle.
	 */
	public final Circle clone () {
		return new Circle (position.x, position.y, radius);
	}
	
	/**
	 * Formated string describing the properties of the 
	 * circle.
	 * 
	 * @return Descriptive string.
	 */
	public final String toString () {
		return "Circle [x:" + position.x + " y:" + position.y + " rad:" + radius + "]";
	}
	
	/**
	 * Creates a new Circle instance in which both given circles 
	 * fit accurately. If one of the circles contains the other one 
	 * - a copy of the large circle will be the result. Use <code>
	 * equals</code> to compare the result with A and B.
	 * 
	 * @param theA Compound circle A
	 * @param theB Compound circle B
	 * 
	 * @return New circle if they don't include each other; or 
	 * a copy of the bigger one.
	 */
	public static final Circle enclose (Circle theA, Circle theB) {	
		// Distance between both centers
		float dist = theA.position.distance (theB.position);
		
		// AB and BA contains check
		if (dist <= theA.radius - theB.radius) {
			return theA.clone ();
		}
		if (dist <= theB.radius - theA.radius) {
			return theB.clone ();
		}
		// Vector between A and B centers defines the angle
		V2f ab = V2f.sub (theA.position, theB.position).normalize ();
		
		// Vectors where the ab 'ray' intersects the outlines
		V2f sub1 = ab.clone ().scale (theA.radius);
		V2f sub2 = ab.clone ().scale (theB.radius).invert ();
		
		// Holds the midpoint of the new circle
		V2f cen	= V2f.sum (theB.position, sub2);
		cen.x += ((theA.position.x + sub1.x) - (theB.position.x + sub2.x)) / 2;
		cen.y += ((theA.position.y + sub1.y) - (theB.position.y + sub2.y)) / 2;
		
		return new Circle (cen, (dist + theA.radius + theB.radius) / 2);
	}
	
	/**
	 * Creates a new set of circles by looking for intersections 
	 * between the instances in the list <code>theCircles</code>. 
	 * If a circle has no intersection at all a copy will be the 
	 * result. If it overlaps with another circle or enclosed group 
	 * a new circle will be returned which includes all intersecting.
	 * 
	 * @param theCircles List of circle instances to group.
	 * 
	 * @return List of circles where all circle or circle-group 
	 * instances are enclosed by one or more circles. If they don't 
	 * overlap the number of elements will be equal to the number 
	 * of elements of <code>theCircles</code>.
	 */
	public static final Circle[] enclose (Circle[] theCircles) {
		Circle[] tmp1 = new Circle[theCircles.length];
		Circle[] tmp2;
		
		boolean skip;
		int ocnt = 0;
		int rcnt = 0;
		
		// Create groups out of original
		for (int i=0; i < theCircles.length; i++) {
			skip = false;
			for (int j=0; j < tmp1.length; j++) {
				if (tmp1[j] != null) {
					if (tmp1[j].contains (theCircles[i])) {
						skip = true;
					}else if (theCircles[i].contains (tmp1[j])) {
						tmp1[j] = theCircles[i].clone ();
						skip = true;
					}else if (theCircles[i].position.distance (tmp1[j].position) <= theCircles[i].radius + tmp1[j].radius) {
						tmp1[j] = enclose (tmp1[j], theCircles[i]);
						skip = true;
					}
				}
			}
			if (!skip) {
				tmp1[ocnt] = theCircles[i].clone ();
				ocnt++;
			}
		}
		
		tmp2 = new Circle[ocnt];
		
		/* Merge overlapping groups by editing the 
		 * properties of one intersecting circle and 
		 * clearing the other one.
		 */
		for (int i=0; i < tmp2.length; i++){
			skip = (tmp1[i] == null);
			if (!skip) {
				for (int j = i; j < tmp2.length; j++){
					
					/* Don't compare a circle with it's 
					 * self or circles that have been 
					 * cleared before in the loop.
					 */
					if (i != j && !skip && tmp1[i] != null && tmp1[j] != null){
						
						// Check for intersection
						if (tmp1[i].position.distance (tmp1[j].position) <= tmp1[i].radius + tmp1[j].radius){
							
							// Merge group A with group B
							tmp2[rcnt]	= enclose (tmp1[i], tmp1[j]);
							
							// Clear the originals
							tmp1[i]		= null;
							tmp1[j]		= null;
							skip		= true;
							rcnt++;
						}
					}
				}
			}
			/* Copy the group if it doesn't 
			 * intersect with other groups.
			 */
			if (!skip) {
				tmp2[rcnt] = tmp1[i];
				rcnt++;
			}
		}
		
		// Copy and return cropped list of results
		Circle[] res = new Circle[rcnt];
		System.arraycopy (tmp2, 0, res, 0, rcnt);
		
		return res;
	}
}